/**
 * Copyright 2007-2012 Arthur Blake
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.log4jdbc.log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.*;
import java.util.regex.Pattern;

import net.sf.log4jdbc.PatterUtil;
import net.sf.log4jdbc.Properties;
import net.sf.log4jdbc.sql.Spy;
import net.sf.log4jdbc.sql.jdbcapi.ConnectionSpy;
import net.sf.log4jdbc.sql.resultsetcollector.ResultSetCollector;

/**
 * @author zhanxb
 */
public class StdoutSpyLogDelegator implements SpyLogDelegator {

    protected static final List<String> prevStackTrace = new ArrayList<String>();
    /**
     * Create a SpyLogDelegator specific to the Standard Out Logging Facade for Java
     *
     */
    public StdoutSpyLogDelegator() {
    }

    private static String nl = System.getProperty("line.separator");
    private boolean isPrint = false;

    /**
     * Determine if any of the 5 log4jdbc spy loggers are turned on (jdbc.audit
     *
     * @return true if any of the 5 spy jdbc/sql loggers are enabled at debug
     *         info or error level.
     */
    public boolean isJdbcLoggingEnabled() {
        return (isPrintTrace());
    }

    public void setPrintFlag(boolean printFlag) {
        isPrint = printFlag;
    }

    public void exceptionOccured(Spy spy, String methodCall, Exception e, String sql, long execTime) {
        String classType = spy.getClassType();
        Integer spyNo = spy.getConnectionNumber();
        String header = spyNo + ". " + classType + "." + methodCall;
        if (sql == null) {
            sqlTimingStdout(header, e);
        } else {
            sql = processSql(sql);

            if (isPrintTrace(true)) {
                sqlTimingStdout(getDebugInfo() + nl + "/*execSQL:*/ " + spyNo + ". " + sql + " {FAILED after "
                        + execTime + " msec}", e);
            } else {
                sqlTimingStdout(
                        header + " FAILED! " + nl + "/*execSQL:*/ " + sql + " {FAILED after " + execTime + " msec}", e);
            }
        }
    }

    public void methodReturned(Spy spy, String methodCall, String returnMsg) {
        // String classType = spy.getClassType();
        // if (isPrintTrace()) {
        // String header = spy.getConnectionNumber() + ". " + classType + "." + methodCall + " returned " + returnMsg;
        // sqlTimingStdout(header + " " + getDebugInfo());
        // }
    }

    public void constructorReturned(Spy spy, String constructionInfo) {
        // not used in this implementation -- yet
    }

    /**
     * Determine if the given sql should be logged or not based on the various
     * DumpSqlXXXXXX flags.
     *
     * @param sql
     *            SQL to test.
     * @return true if the SQL should be logged, false if not.
     */
    private boolean shouldSqlBeLogged(String sql) {
        if (sql == null) {
            return false;
        }
        sql = sql.trim();

        if (sql.length() < 6) {
            return false;
        }
        sql = sql.substring(0, 6).toLowerCase();
        return (Properties.isDumpSqlSelect() && "select".equals(sql))
                || (Properties.isDumpSqlInsert() && "insert".equals(sql))
                || (Properties.isDumpSqlUpdate() && "update".equals(sql))
                || (Properties.isDumpSqlDelete() && "delete".equals(sql))
                || (Properties.isDumpSqlCreate() && "create".equals(sql));
    }

    public void sqlOccurred(Spy spy, String methodCall, String sql) {
        if (!Properties.isDumpSqlFilteringOn() || shouldSqlBeLogged(sql)) {
        }
    }

    /**
     * Break an SQL statement up into multiple lines in an attempt to make it
     * more readable
     *
     * @param sql
     *            SQL to break up.
     * @return SQL broken up into multiple lines
     */
    private String processSql(String sql) {
        if (sql == null) {
            return null;
        }

        if (Properties.isSqlTrim()) {
            sql = sql.trim();
        }

        StringBuilder output = new StringBuilder();

        if (Properties.getDumpSqlMaxLineLength() <= 0) {
            output.append(sql);
        } else {
            // insert line breaks into sql to make it more readable
            StringTokenizer st = new StringTokenizer(sql);
            String token;
            int linelength = 0;

            while (st.hasMoreElements()) {
                token = (String) st.nextElement();

                output.append(token);
                linelength += token.length();
                output.append(" ");
                linelength++;
                if (linelength > Properties.getDumpSqlMaxLineLength()) {
                    output.append(nl);
                    linelength = 0;
                }
            }
        }

        if (Properties.isDumpSqlAddSemicolon()) {
            output.append(";");
        }

        String stringOutput = output.toString();

        if (Properties.isTrimExtraBlankLinesInSql()) {
            LineNumberReader lineReader = new LineNumberReader(new StringReader(stringOutput));

            output = new StringBuilder();

            int contiguousBlankLines = 0;
            try {
                while (true) {
                    String line = lineReader.readLine();
                    if (line == null) {
                        break;
                    }

                    // is this line blank?
                    if (line.trim().length() == 0) {
                        contiguousBlankLines++;
                        // skip contiguous blank lines
                        if (contiguousBlankLines > 1) {
                            continue;
                        }
                    } else {
                        contiguousBlankLines = 0;
                        output.append(line);
                    }
                    output.append(nl);
                }
            } catch (IOException e) {
                // since we are reading from a buffer, this isn't likely to
                // happen,
                // but if it does we just ignore it and treat it like its the
                // end of the stream
            }
            stringOutput = output.toString();
        }

        return stringOutput;
    }

    /**
     * Special call that is called only for JDBC method calls that contain SQL.
     *
     * @param spy
     *            the Spy wrapping the class where the SQL occurred.
     *
     * @param execTime
     *            how long it took the SQL to run, in milliseconds.
     *
     * @param methodCall
     *            a description of the name and call parameters of the method
     *            that generated the SQL.
     *
     * @param sql
     *            SQL that occurred.
     */
    public void sqlTimingOccurred(Spy spy, long execTime, String methodCall, String sql) {
        if (isPrintTrace(true)) {
            if (Properties.isSqlTimingErrorThresholdEnabled()
                    && execTime >= Properties.getSqlTimingErrorThresholdMsec()) {
                sqlTimingStdout(getStackTraceInfo() + nl + "Error!!! SQL excute more than "
                        + Properties.getSqlTimingErrorThresholdMsec() / 1000 + " second" + nl
                        + buildSqlTimingDump(spy, execTime, methodCall, sql, false));
            } else if (Properties.isSqlTimingWarnThresholdEnabled()
                    && execTime >= Properties.getSqlTimingWarnThresholdMsec()) {
                sqlTimingStdout(getStackTraceInfo() + nl + "Warn!!! SQL excute more than "
                        + Properties.getSqlTimingWarnThresholdMsec() / 1000 + " second" + nl
                        + buildSqlTimingDump(spy, execTime, methodCall, sql, false));
            } else {
                sqlTimingStdout(getStackTraceInfo() + nl + buildSqlTimingDump(spy, execTime, methodCall, sql, false));
            }
        }
    }

    /**
     * Helper method to quickly build a SQL timing dump output String for
     * logging.
     *
     * @param spy
     *            the Spy wrapping the class where the SQL occurred.
     *
     * @param execTime
     *            how long it took the SQL to run, in milliseconds.
     *
     * @param methodCall
     *            a description of the name and call parameters of the method
     *            that generated the SQL.
     *
     * @param sql
     *            SQL that occurred.
     *
     * @param debugInfo
     *            if true, include debug info at the front of the output.
     *
     * @return a SQL timing dump String for logging.
     */
    private String buildSqlTimingDump(Spy spy, long execTime, String methodCall, String sql, boolean debugInfo) {
        StringBuffer out = new StringBuffer();

        if (debugInfo) {
            out.append(getDebugInfo());
            out.append(nl);
            out.append(spy.getConnectionNumber());
            out.append(". ");
        }
        // NOTE: if both sql dump and sql timing dump are on, the processSql
        // algorithm will run TWICE once at the beginning and once at the end
        // this is not very efficient but usually
        // only one or the other dump should be on and not both.

        sql = processSql(sql);
        out.append("/*execSQL:*/ ");
        if(Properties.isShowTime()){
            out.append("/*{Thread:[");
            out.append(Thread.currentThread().getName());
            out.append("],executed:");
            out.append(execTime);
            out.append("(ms)}*/ ");
        }
        out.append(sql);
        out.append("  /*{JDBC ConnectionNumber:");
        out.append(spy.getConnectionNumber());
        out.append(". ");
        out.append("JDBC interface method:");
        out.append(spy.getClassType());
        out.append(".");
        out.append(methodCall);
        out.append(" executed in ");
        out.append(execTime);
        out.append(" msec}*/");
        out.append(nl);

        return out.toString();
    }

    /**
     * Get debugging info - the module and line number that called the logger
     * version that prints the stack trace information from the point just
     * before we got it (net.sf.log4jdbc)
     *
     * if the optional log4jdbc.debug.stack.prefix system property is defined
     * then the last call point from an application is shown in the debug trace
     * output, instead of the last direct caller into log4jdbc
     *
     * @return debugging info for whoever called into JDBC from within the
     *         application.
     */
    private static String getDebugInfo() {
        Throwable t = new Throwable();
        t.fillInStackTrace();

        StackTraceElement[] stackTrace = t.getStackTrace();

        if (stackTrace != null) {
            String className;

            StringBuffer dump = new StringBuffer();

            /**
             * The DumpFullDebugStackTrace option is useful in some situations
             * when we want to see the full stack trace in the debug info- watch
             * out though as this will make the logs HUGE!
             */
            if (Properties.isDumpFullDebugStackTrace()) {
                boolean first = true;
                for (int i = 0; i < stackTrace.length; i++) {
                    className = stackTrace[i].getClassName();
                    if (!className.startsWith("net.sf.log4jdbc")) {
                        if (first) {
                            first = false;
                        } else {
                            dump.append("  ");
                        }
                        dump.append("at ");
                        dump.append(stackTrace[i]);
                        dump.append(nl);
                    }
                }
            } else {
                dump.append(" ");
                int firstLog4jdbcCall = 0;
                int lastApplicationCall = 0;

                for (int i = 0; i < stackTrace.length; i++) {
                    className = stackTrace[i].getClassName();
                    if (className.startsWith("net.sf.log4jdbc")) {
                        firstLog4jdbcCall = i;
                    } else if (Properties.isTraceFromApplication()
                            && Pattern.matches(Properties.getDebugStackPrefix(), className)) {
                        lastApplicationCall = i;
                        break;
                    }
                }
                int j = lastApplicationCall;

                if (j == 0) // if app not found, then use whoever was the last
                // guy that called a log4jdbc class.
                {
                    j = 1 + firstLog4jdbcCall;
                }

                dump.append(stackTrace[j].getClassName()).append(".").append(stackTrace[j].getMethodName()).append("(")
                        .append(stackTrace[j].getFileName()).append(":").append(stackTrace[j].getLineNumber())
                        .append(")");
            }

            return dump.toString();
        }
        return null;
    }

    public void debug(String msg) {
        // sqlTimingStdout(msg);
    }

    public void connectionOpened(Spy spy, long execTime) {
        // we just delegate to the already existing method,
        // so that we do not change the behavior of the standard implementation
        this.connectionOpened(spy);
    }

    /**
     * Called whenever a new connection spy is created.
     *
     * @param spy
     *            ConnectionSpy that was created.
     */
    private void connectionOpened(Spy spy) {
        if (isPrintTrace() || isPrint) {
            connectStdout(spy.getConnectionNumber() + ". Connection opened " + getDebugInfo());
            connectStdout(ConnectionSpy.getOpenConnectionsDump());
        }
    }

    public void connectionClosed(Spy spy, long execTime) {
        // we just delegate to the already existing method,
        // so that we do not change the behavior of the standard implementation
        this.connectionClosed(spy);
    }

    /**
     * Called whenever a connection spy is closed.
     *
     * @param spy
     *            ConnectionSpy that was closed.
     */
    private void connectionClosed(Spy spy) {
        if (isPrintTrace() || isPrint) {
            connectStdout(spy.getConnectionNumber() + ". Connection closed " + getDebugInfo());
            connectStdout(ConnectionSpy.getOpenConnectionsDump());
        }
    }

    public void connectionAborted(Spy spy, long execTime) {
        this.connectionAborted(spy);
    }

    /**
     * Called whenever a connection spy is aborted.
     *
     * @param spy
     *            ConnectionSpy that was aborted.
     */
    private void connectionAborted(Spy spy) {
        if (isPrintTrace() || isPrint) {
            connectStdout(spy.getConnectionNumber() + ". Connection aborted " + getDebugInfo());
            connectStdout(ConnectionSpy.getOpenConnectionsDump());
        }
    }

    public boolean isResultSetCollectionEnabled() {
        return isPrintTrace();
    }

    public boolean isResultSetCollectionEnabledWithUnreadValueFillIn() {
        return isPrintTrace(true);
    }

    public void resultSetCollected(ResultSetCollector resultSetCollector) {
        // String resultsToPrint = new ResultSetCollectorPrinter().getResultSetToPrint(resultSetCollector);
        // resultStdout(resultsToPrint);
    }

    /**
     * New method;
     *
     * @return
     */
    private boolean isPrintTrace(boolean onlyHttp) {
        Throwable t = new Throwable();
        t.fillInStackTrace();
        boolean retval = false;
        StackTraceElement[] stackTrace = t.getStackTrace();
        List<StackTraceElement> stacks = new ArrayList<StackTraceElement>();
        if (stackTrace != null) {
            boolean onlyHttpResult = !onlyHttp;
            for (StackTraceElement stackTraceElement : stackTrace) {
                boolean onlyHttpInvoke =
                        onlyHttp && Properties.getBooleanOption(System.getProperties(), "onlyHttpInvoke", true);
                if (onlyHttpInvoke && isHttp(stackTraceElement)) {
                    onlyHttpResult = true;
                }
                if (PatterUtil.classMatch(stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName(),
                        "expkgs")) {
                    return false;
                }
                if (PatterUtil.classMatch(stackTraceElement.getClassName() + "." + stackTraceElement.getMethodName(),
                        "inpkgs")) {
                    stacks.add(stackTraceElement);
                }
            }
            retval = (!stacks.isEmpty()) && onlyHttpResult;
        }
        return retval;
    }
    private boolean isPrintTrace(){
        return isPrintTrace(false);
    }


    private boolean isHttp(StackTraceElement element){
        List<String> list = Arrays.asList("javax.servlet.http.HttpServlet", "javax.servlet.Servlet");
        boolean result = list.contains(element.getClassName());
        String name = Thread.currentThread().getName();
        result = result || (name.toLowerCase().contains("http")&&!name.toLowerCase().contains("main"));
        return result;
    }
    /**
     * New method;
     *
     * @return StackTraceInfo;
     */
    private String getStackTraceInfo() {
        Throwable t = new Throwable();
        t.fillInStackTrace();

        StackTraceElement[] stackTrace = t.getStackTrace();

        if (stackTrace != null) {
            StringBuffer dump = new StringBuffer();
            boolean first = true;
            dump.append("=============== \u6253\u5370\u6267\u884c\u004a\u0044\u0042\u0043\u7684\u7ebf\u7a0b[")
                    .append(Thread.currentThread().getName()).append("]\u5806\u6808\u8c03\u7528\u94fe ===============");
            for (int i = stackTrace.length - 1; i > 0; i--) {
                if (PatterUtil.classMatch(stackTrace[i].getClassName() + "." + stackTrace[i].getMethodName(),
                        "inpkgs")) {
                    dump.append(nl);
                    if (first) {
                        first = false;
                    } else {
                        dump.append("  ");
                    }
                    dump.append("at ");
                    dump.append(stackTrace[i]);
                }
            }
            return dump.toString();
        }
        return null;
    }

    /**
     * get compressed stack information
     * @return
     */
    private String getCompressStackTraceInfo() {
        Throwable t = new Throwable();
        t.fillInStackTrace();

        StackTraceElement[] stackTrace = t.getStackTrace();
        if (stackTrace != null) {
            StringBuffer dump = new StringBuffer();
            boolean first = true;
            int j = 0;
            for (int i = stackTrace.length - 1; i > 0; i--) {
                if (PatterUtil.classMatch(stackTrace[i].getClassName() + "." + stackTrace[i].getMethodName(),
                        "inpkgs")) {
                    if (j >= prevStackTrace.size()) {
                        prevStackTrace.add(j, stackTrace[i].toString());
                        if (first) {
                            dump.append(
                                    "=============== \u6253\u5370\u6267\u884c\u0053\u0051\u004c\u7684\u5806\u6808\u8c03\u7528\u4fe1\u606f\u5217\u8868\uff1a===============");
                            dump.append(nl);
                            first = false;
                        } else {
                            dump.append(nl);
                            dump.append("  ");
                        }
                        dump.append("at ");
                        dump.append(stackTrace[i]);
                    } else if (j < prevStackTrace.size() && !prevStackTrace.get(j).equals(stackTrace[i].toString())) {
                        prevStackTrace.set(j, stackTrace[i].toString());
                        if (first) {
                            dump.append(
                                    "=============== \u6253\u5370\u6267\u884c\u0053\u0051\u004c\u7684\u5806\u6808\u8c03\u7528\u4fe1\u606f\u5217\u8868\uff1a===============");
                            dump.append(nl);
                            first = false;
                        } else {
                            dump.append(nl);
                            dump.append("  ");
                        }
                        dump.append("at ");
                        dump.append(stackTrace[i]);
                    } else {
                        first = false;
                    }
                    j++;
                }
            }
            if (j < prevStackTrace.size()) {
                for (int k = prevStackTrace.size() - 1; k > j; k--) {
                    prevStackTrace.remove(k);
                }
            }
            return dump.toString();
        }
        return null;
    }

    private static void sqlTimingStdout(final String sqlInfo, final Exception e) {
        sqlTimingStdout(sqlInfo + e.getMessage());
    }

    private static void sqlTimingStdout(final String sqlInfo) {
        if (Properties.getJdkLogger() != null)
        {
            Properties.getJdkLogger().info(sqlInfo);
        } else
        {
            System.out.println(sqlInfo);
        }
    }

    private static void connectStdout(final String sqlInfo) {
        if (Properties.getJdkLogger() != null)
        {
            Properties.getJdkLogger().info(sqlInfo);
        } else
        {
            System.out.println(sqlInfo);
        }
    }

    private static void resultStdout(final String sqlInfo) {
        if (Properties.getJdkLogger() != null)
        {
            Properties.getJdkLogger().info(sqlInfo);
        } else
        {
            System.out.println(sqlInfo);
        }
    }
}
